package tcpclient.v2;

import frame.laxcen.BaseFrame;
import frame.laxcen.request.IRequest;
import frame.laxcen.response.IResponse;
import frame.laxcen.util.FrameUtil;
import util.ByteBufferUtil;
import util.FlowUtil;
import util.Ref;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.*;
import java.util.function.Predicate;

public class RFIDClient {

    private volatile boolean isRunning = true;

    private final AsynchronousSocketChannel channel;

    private final ByteBuffer frameBuffer;

    private final LinkedBlockingQueue<BaseFrame> queue;

    public RFIDClient(int maxFrameSize, int maxQueueSize) throws IOException {

        this.channel = AsynchronousSocketChannel.open();

        this.frameBuffer = ByteBuffer.allocate(maxFrameSize);

        this.queue = new LinkedBlockingQueue<>(maxQueueSize);
    }

    public boolean tryConnect(InetSocketAddress address) {

        return tryConnect(address, 3000);
    }

    public boolean tryConnect(InetSocketAddress address, long millisecond) {

        try {

            channel.connect(address).get(millisecond, TimeUnit.MILLISECONDS);

            channel.read(frameBuffer, this, new CallBackForRead());

        } catch (Exception e) { return false; }

        return true;
    }

    public <T extends IRequest> boolean trySend(T frame, long millisecond) {

        try {

            channel.write(ByteBuffer.wrap(frame.getFrameBytes())).get(millisecond, TimeUnit.MILLISECONDS);

        } catch (Exception e) { return false; }

        return true;
    }

    public boolean tryClose() {

        try {

            channel.close();

            isRunning = false;

        } catch (Exception e) { return false; }

        return true;
    }

    public Ref<BaseFrame> waitForArbitraryResponse(long millisecond) {

        List<BaseFrame> response = waitForResponseList(millisecond, (ignored) -> true, 1);

        return response.size() >= 1 ? Ref.of(response.get(0)) : Ref.empty();
    }

    public Ref<BaseFrame> waitForSpecialResponse(long millisecond, Class<? extends IResponse> type) {

        List<BaseFrame> response = waitForResponseList(millisecond, (frame) -> frame.isResponse(type), 1);

        return response.size() >= 1 ? Ref.of(response.get(0)) : Ref.empty();
    }

    public List<BaseFrame> waitForResponseList(long millisecond) {

        return waitForResponseList(millisecond, (ignored) -> true, Integer.MAX_VALUE);
    }

    public void waitForResponseListSplitByUploadFlag(long millisecond, Ref<List<BaseFrame>> isNotUpload, Ref<List<BaseFrame>> isUpload) {

        Predicate<BaseFrame> isUploadFrame = (frame) -> frame.CW.IsUpload;

        waitForSplitResponseList(millisecond, isNotUpload, Integer.MAX_VALUE, isUpload, Integer.MAX_VALUE, isUploadFrame.negate());
    }

    private void waitForSplitResponseList(long millisecond, Ref<List<BaseFrame>> trueResult, int limitOfTrueResult, Ref<List<BaseFrame>> falseResult, int limitOfFalseResult, Predicate<BaseFrame> predicate) {

        List<BaseFrame> trueList = new LinkedList<>(), falseList = new LinkedList<>();

        while(millisecond > 0) {

            final long waitTime = millisecond, begin = System.currentTimeMillis();

            Ref<BaseFrame> ref = Ref.empty();

            FlowUtil.silent(() -> ref.swap(queue.poll(waitTime, TimeUnit.MILLISECONDS)));

            millisecond -= System.currentTimeMillis() - begin;

            if(ref.isPresent()) FrameUtil.dispatch(ref.get(), trueList, falseList, predicate);

            while(trueList.size() > limitOfTrueResult) trueList.remove(trueList.size() - 1);

            while(falseList.size() > limitOfFalseResult) falseList.remove(falseList.size() - 1);

            if(trueList.size() >= limitOfTrueResult && falseList.size() >= limitOfFalseResult) break;
        }

        if(trueList.size() > 0) trueResult.swap(trueList);

        if(falseList.size() > 0) falseResult.swap(falseList);
    }

    private List<BaseFrame> waitForResponseList(long millisecond, Predicate<BaseFrame> predicate, int limit) {

        List<BaseFrame> response = new LinkedList<>();

        while(millisecond > 0) {

            final long waitTime = millisecond, begin = System.currentTimeMillis();

            Ref<BaseFrame> ref = Ref.empty();

            FlowUtil.silent(() -> ref.swap(queue.poll(waitTime, TimeUnit.MILLISECONDS)));

            millisecond -= System.currentTimeMillis() - begin;

            if(ref.isPresent()) response.add(ref.get());

            if(response.size() > 0) response.removeIf(predicate.negate());

            if(response.size() >= limit) break;
        }

        return response;
    }

    private static class CallBackForRead implements CompletionHandler<Integer, RFIDClient> {

        @Override
        public void completed(Integer ignored, RFIDClient client) {

            FrameUtil.disjoint(ByteBufferUtil.readAndClear(client.frameBuffer)).forEach((frame) -> FlowUtil.silent(() -> client.queue.put(frame)));

            if(client.isRunning) client.channel.read(client.frameBuffer, client, new CallBackForRead());
        }

        @Override
        public void failed(Throwable exc, RFIDClient client) { exc.printStackTrace(); }
    }
}
